using System;
using System.Collections.Generic;
using UnityEngine;

namespace Gj
{
    public struct HexCube
    {
        [SerializeField] private int q, r;

        public HexMetrics Metrics { get; private set; }

        private int s => -q - r;
        public const int EVEN = 1;
        public const int ODD = -1;

        private static readonly HexCube[] directions =
        {
            new(1, -1, 0), new(1, 0, -1), new(0, 1, -1),
            new(-1, 1, 0), new(-1, 0, 1), new(0, -1, 1)
        };

        private static readonly int[][] directionMap =
        {
            new[] { -1, -1, -1, 10, -1 },
            new[] { -1, 11, 4, 3, 9 },
            new[] { -1, 5, -1, 2, -1 },
            new[] { 6, 0, 1, 8, -1 },
            new[] { -1, 7, -1, -1, -1 }
        };

        private static readonly Func<HexCube, HexCube>[] RotateMap = new Func<HexCube, HexCube>[5]
        {
            hexCube =>
            {
                var q = hexCube.q;
                hexCube.q = -hexCube.s;
                hexCube.r = -q;
                return hexCube;
            },
            hexCube =>
            {
                var s = hexCube.s;
                hexCube.q = hexCube.r;
                hexCube.r = s;
                return hexCube;
            },
            hexCube =>
            {
                var s = hexCube.s;
                hexCube.q = -hexCube.q;
                hexCube.r = -hexCube.r;
                return hexCube;
            },
            hexCube =>
            {
                var q = hexCube.q;
                hexCube.q = hexCube.s;
                hexCube.r = q;
                return hexCube;
            },
            hexCube =>
            {
                var s = hexCube.s;
                hexCube.q = -hexCube.r;
                hexCube.r = -s;
                return hexCube;
            }
        };

        private HexCube(int q, int r, int s, HexMetrics metrics = null)
        {
            if (metrics == null) metrics = HexMetrics.Default;
            this.q = q;
            this.r = r;
            Metrics = metrics;
        }

        public bool IsExist()
        {
            return Metrics != null;
        }

        public Vector2Int ToOffset()
        {
            return Metrics.CubeToOffset(this);
        }

        public Vector2Int ToVector2()
        {
            return new Vector2Int(q, r);
        }

        public Vector3 ToPosition()
        {
            var M = Metrics.orientation;
            var x = (M.f0 * q + M.f1 * r) * Metrics.size.x;
            var y = (M.f2 * q + M.f3 * r) * Metrics.size.y;
            return new Vector3((float)x + Metrics.origin.x, 0, (float)y + Metrics.origin.y);
        }

        public override string ToString()
        {
            return string.Format("({0}, {1}, {2})", q, r, s);
        }

        public void Move(HexCube cube)
        {
            q += cube.q;
            r += cube.r;
        }

        public static HexCube FromPosition(Vector3 p, HexMetrics metrics = null)
        {
            if (metrics == null) metrics = HexMetrics.Default;
            var M = metrics.orientation;
            var x = (p.x - metrics.origin.x) / metrics.size.x;
            var y = (p.z - metrics.origin.y) / metrics.size.y;
            var q = M.b0 * x + M.b1 * y;
            var r = M.b2 * x + M.b3 * y;
            return HexRound(q, r, -q - r, metrics);
        }

        public static HexCube HexRound(double _q, double _r, double _s, HexMetrics metrics)
        {
            var q = (int)Math.Round(_q);
            var r = (int)Math.Round(_r);
            var s = (int)Math.Round(_s);
            var q_diff = Math.Abs(q - _q);
            var r_diff = Math.Abs(r - _r);
            var s_diff = Math.Abs(s - _s);
            if (q_diff > r_diff && q_diff > s_diff)
                q = -r - s;
            else if (r_diff > s_diff)
                r = -q - s;
            else
                s = -q - r;
            return new HexCube(q, r, s, metrics);
        }

        public static HexCube FromOffset(Vector2Int offset, HexMetrics metrics = null)
        {
            if (metrics == null) metrics = HexMetrics.Default;
            return metrics.OffsetToCube(offset);
        }

        public static HexCube FromCube(Vector2Int cube, HexMetrics metrics = null)
        {
            return new HexCube(cube.x, cube.y, -cube.x - cube.y, metrics ?? HexMetrics.Default);
        }

        public static HexCube Direction(int direction /* 0 to 5 */)
        {
            return directions[direction];
        }

        public static HexCube Neighbor(HexCube hex, int direction)
        {
            return AddOffset(hex, Direction(direction));
        }

        public static HexCube Scale(int direction, int distance, HexMetrics metrics = null)
        {
            var cube = new HexCube(0, 0, 0, metrics);
            var d = Direction(direction);
            for (var i = 0; i < distance; i++) cube.Move(d);

            return cube;
        }

        public static float Distance(HexCube a, HexCube b)
        {
            return (Math.Abs(a.q - b.q) + Math.Abs(a.r - b.r) + Math.Abs(a.s - b.s)) / 2;
        }

        public static HexCube AddOffset(HexCube target, int q, int r, int s)
        {
            return new HexCube { q = target.q + q, r = target.r + r, Metrics = target.Metrics };
        }

        public static HexCube AddOffset(HexCube target, HexCube other)
        {
            return new HexCube { q = target.q + other.q, r = target.r + other.r, Metrics = target.Metrics };
        }

        public static HexCube RemoveOffset(HexCube target, int q, int r, int s)
        {
            return new HexCube { q = target.q - q, r = target.r - r, Metrics = target.Metrics };
        }

        public static HexCube RemoveOffset(HexCube target, HexCube other)
        {
            return new HexCube { q = target.q - other.q, r = target.r - other.r, Metrics = target.Metrics };
        }

        public static Vector2Int GetPoint(Vector2Int source, HexDirectionRow direction)
        {
            var target = Vector2Int.zero;
            AddDirection(source, direction, ref target);
            return target;
        }

        public static Vector2Int ToEventQ(HexCube h)
        {
            var col = h.q;
            var row = h.r + (h.q + (h.q & 1)) / 2;
            return new Vector2Int(col, row);
        }

        public static Vector2Int ToOddQ(HexCube h)
        {
            var col = h.q;
            var row = h.r + (h.q - (h.q & 1)) / 2;
            return new Vector2Int(col, row);
        }

        public static Vector2Int ToEvenR(HexCube h)
        {
            var col = h.q + (h.r + (h.r & 1)) / 2;
            var row = h.r;
            return new Vector2Int(col, row);
        }

        public static Vector2Int ToOddR(HexCube h)
        {
            var col = h.q + (h.r - (h.r & 1)) / 2;
            var row = h.r;
            return new Vector2Int(col, row);
        }

        public static HexCube OddRToCube(Vector2Int h, HexMetrics metrics = null)
        {
            var q = h.x - (h.y - (h.y & 1)) / 2;
            var r = h.y;
            var s = -q - r;
            return new HexCube(q, r, s, metrics);
        }

        public static HexCube EvenRToCube(Vector2Int h, HexMetrics metrics = null)
        {
            var q = h.x - (h.y + (h.y & 1)) / 2;
            var r = h.y;
            var s = -q - r;
            return new HexCube(q, r, s, metrics);
        }

        public static HexCube OddQToCube(Vector2Int h, HexMetrics metrics = null)
        {
            var q = h.x;
            var r = h.y - (h.x - (h.x & 1)) / 2;
            var s = -q - r;
            return new HexCube(q, r, s, metrics);
        }

        public static HexCube EvenQToCube(Vector2Int h, HexMetrics metrics = null)
        {
            var q = h.x;
            var r = h.y - (h.x + (h.x & 1)) / 2;
            var s = -q - r;
            return new HexCube(q, r, s, metrics);
        }

        public static byte ToDirection(HexCube source, HexCube target)
        {
            // Debug.LogFormat("source: {0}, {1}, target: {2}, {3}", source.q, source.r, target.q, target.r);
            var a = directionMap[target.q - source.q + 2][target.r - source.r + 2];
            if (a == -1) throw new Exception("Direction error");
            return (byte)a;
        }

        public static int ToDirectionAll(HexCube source, HexCube target)
        {
            try
            {
                return directionMap[target.q - source.q + 2][target.r - source.r + 2];
            }
            catch (Exception e)
            {
                return -1;
            }
        }

        public static void WithDirection(int x, int y, HexDirectionColumn direction, ref Vector2Int target)
        {
            var a = HexDirectionExtensions.EVENR[y % 1][(byte)direction];
            target.x = a[0];
            target.y = a[1];
        }

        public static void WithDirection(Vector2Int offset, HexDirectionColumn direction, ref Vector2Int target)
        {
            WithDirection(offset.x, offset.y, direction, ref target);
        }

        public static void AddDirection(Vector2Int offset, HexDirectionColumn direction, ref Vector2Int target)
        {
            WithDirection(offset, direction, ref target);
            target.x += offset.x;
            target.y += offset.y;
        }

        public static void WithDirection(int x, int y, HexDirectionColumnRow direction, ref Vector2Int target)
        {
            var a = HexDirectionExtensions.EVENR[y % 1 + 2][(byte)direction];
            target.x = a[0];
            target.y = a[1];
        }

        public static void WithDirection(Vector2Int offset, HexDirectionColumnRow direction, ref Vector2Int target)
        {
            WithDirection(offset.x, offset.y, direction, ref target);
        }

        public static void AddDirection(Vector2Int offset, HexDirectionColumnRow direction, ref Vector2Int target)
        {
            WithDirection(offset, direction, ref target);
            target.x += offset.x;
            target.y += offset.y;
        }

        public static void WithDirection(int x, int y, HexDirectionRow direction, ref Vector2Int target)
        {
            var a = HexDirectionExtensions.EVENQ[x & 1][(byte)direction];
            target.x = a[0];
            target.y = a[1];
        }

        public static void WithDirectionDouble(HexDirectionRowDouble direction, ref Vector2Int target)
        {
            var a = HexDirectionExtensions.EVENQ[0][(byte)direction];
            target.x = a[0];
            target.y = a[1];
        }

        public static void WithDirection(Vector2Int offset, HexDirectionRow direction, ref Vector2Int target)
        {
            WithDirection(offset.x, offset.y, direction, ref target);
        }

        public static void AddDirection(Vector2Int offset, HexDirectionRow direction, ref Vector2Int target)
        {
            WithDirection(offset, direction, ref target);
            target.x += offset.x;
            target.y += offset.y;
        }

        public static void WithDirection(int x, int y, HexDirectionRowColumn direction, ref Vector2Int target)
        {
            var a = HexDirectionExtensions.EVENQ[(x & 1) + 2][(byte)direction];
            target.x = a[0];
            target.y = a[1];
        }

        public static void WithDirection(Vector2Int offset, HexDirectionRowColumn direction, ref Vector2Int target)
        {
            WithDirection(offset.x, offset.y, direction, ref target);
        }

        public static void AddDirection(Vector2Int offset, HexDirectionRowColumn direction, ref Vector2Int target)
        {
            WithDirection(offset, direction, ref target);
            target.x += offset.x;
            target.y += offset.y;
        }

        public static Vector2Int[] DistanceRange(Vector2Int offsetCenter, int distance, HexMetrics metrics = null)
        {
            var cube = FromOffset(offsetCenter, metrics);
            var list = new List<Vector2Int>();
            for (var q = -distance; q <= distance; q++)
            {
                var r1 = Math.Max(-distance, -q - distance);
                var r2 = Math.Min(distance, -q + distance);
                for (var r = r1; r <= r2; r++) list.Add(AddOffset(cube, q, r, -q - r).ToOffset());
            }

            return list.ToArray();
        }

        public static Vector2Int[] BaseRange(Vector2Int offsetCenter, HexMetrics metrics = null)
        {
            if (metrics == null) metrics = HexMetrics.Default;
            var list = new Vector2Int[6];
            switch (metrics.Mode)
            {
                case HexMode.Row:
                    for (var i = 0; i < 6; i++) AddDirection(offsetCenter, (HexDirectionRow)i, ref list[i]);
                    break;
                case HexMode.Column:
                    for (var i = 0; i < 6; i++) AddDirection(offsetCenter, (HexDirectionColumn)i, ref list[i]);
                    break;
            }

            return list;
        }

        public static Vector2Int[] DistanceRing(Vector2Int offsetCenter, int distance, HexMetrics metrics = null)
        {
            var cube = FromOffset(offsetCenter, metrics);
            var list = new List<Vector2Int>();
            cube = AddOffset(cube, Scale(4, distance, metrics));
            for (var i = 0; i < 6; i++)
            for (var j = 0; j < distance; j++)
            {
                list.Add(cube.ToOffset());
                cube = Neighbor(cube, i);
            }

            return list.ToArray();
        }

        public static Vector2Int AddOffset(Vector2Int o, Vector2Int position, HexMetrics metrics = null)
        {
            var cube = FromOffset(o, metrics);
            var tmp = FromOffset(position, metrics);
            var result = AddOffset(tmp, cube).ToOffset();

            return result;
        }

        public static Vector2Int[] AddOffset(Vector2Int o, Vector2Int[] positions, HexMetrics metrics = null)
        {
            var cube = FromOffset(o, metrics);
            var result = new Vector2Int[positions.Length];
            for (var i = 0; i < positions.Length; i++)
            {
                var tmp = FromOffset(positions[i], metrics);
                result[i] = AddOffset(tmp, cube).ToOffset();
            }

            return result;
        }

        public static Vector2Int RemoveOffset(Vector2Int o, Vector2Int position, HexMetrics metrics = null)
        {
            var cube = FromOffset(o, metrics);
            var tmp = FromOffset(position, metrics);
            var result = RemoveOffset(tmp, cube).ToOffset();

            return result;
        }

        public static Vector2Int[] RemoveOffset(Vector2Int o, Vector2Int[] positions, HexMetrics metrics = null)
        {
            var cube = FromOffset(o, metrics);
            var result = new Vector2Int[positions.Length];
            for (var i = 0; i < positions.Length; i++)
            {
                var tmp = FromOffset(positions[i], metrics);
                result[i] = RemoveOffset(tmp, cube).ToOffset();
            }

            return result;
        }

        public static Vector2Int[] Rotate(Vector2Int o, Vector2Int[] positions, int angle, HexMetrics metrics = null)
        {
            var cube = FromOffset(o, metrics);
            var result = new Vector2Int[positions.Length];
            var index = (angle / 60 + 5) % 6;
            var transform = RotateMap[index];
            for (var i = 0; i < positions.Length; i++)
            {
                var tmp = RemoveOffset(FromOffset(positions[i], metrics), cube);
                tmp = transform(tmp);
                result[i] = AddOffset(tmp, cube).ToOffset();
            }

            return result;
        }

        public static Vector2Int Rotate(Vector2Int o, Vector2Int position, int angle, HexMetrics metrics = null)
        {
            if (angle == 0) return position;
            var cube = FromOffset(o, metrics);
            var index = (angle / 60 + 5) % 6;
            var transform = RotateMap[index];
            var tmp = RemoveOffset(FromOffset(position, metrics), cube);
            tmp = transform(tmp);
            return AddOffset(tmp, cube).ToOffset();
        }
    }
}